diff options
Diffstat (limited to 'app/[lng]/evcp/(evcp)/polices/page.tsx')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/polices/page.tsx | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/polices/page.tsx b/app/[lng]/evcp/(evcp)/polices/page.tsx new file mode 100644 index 00000000..46a9e87a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/polices/page.tsx @@ -0,0 +1,238 @@ +// app/admin/policies/page.tsx (서버 컴포넌트) +import { Suspense } from 'react' +import { Metadata } from 'next' +import { eq, desc } from 'drizzle-orm' +import db from '@/db/db' +import { policyVersions } from '@/db/schema' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { FileText, Shield, Calendar, User, Clock } from 'lucide-react' +import { PolicyManagementClient } from '@/components/polices/policy-management-client' + +export const metadata: Metadata = { + title: '정책 관리 | eVCP Admin', + description: '개인정보 처리방침 및 이용약관 관리' +} + +// 정책 데이터 조회 함수 +async function getPoliciesData() { + try { + // 현재 활성 정책들 + const currentPolicies = await db + .select() + .from(policyVersions) + .where(eq(policyVersions.isCurrent, true)) + .orderBy(policyVersions.policyType) + + // 전체 정책 히스토리 + const allPolicies = await db + .select() + .from(policyVersions) + .orderBy(desc(policyVersions.createdAt)) + + // 정책 타입별로 그룹화 + const policiesByType = { + privacy_policy: allPolicies.filter(p => p.policyType === 'privacy_policy'), + terms_of_service: allPolicies.filter(p => p.policyType === 'terms_of_service') + } + + // 현재 정책 맵 + const currentPolicyMap = {} + currentPolicies.forEach(policy => { + currentPolicyMap[policy.policyType] = policy + }) + + return { + currentPolicies: currentPolicyMap, + allPolicies: policiesByType, + stats: { + totalVersions: allPolicies.length, + privacyVersions: policiesByType.privacy_policy.length, + termsVersions: policiesByType.terms_of_service.length, + lastUpdate: allPolicies[0]?.createdAt || null + } + } + } catch (error) { + console.error('Failed to fetch policies:', error) + return { + currentPolicies: {}, + allPolicies: { privacy_policy: [], terms_of_service: [] }, + stats: { totalVersions: 0, privacyVersions: 0, termsVersions: 0, lastUpdate: null } + } + } +} + +export default async function PoliciesPage() { + const data = await getPoliciesData() + + return ( + <div className="container mx-auto py-6 space-y-6"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div> + <h1 className="text-3xl font-bold tracking-tight">정책 관리</h1> + <p className="text-muted-foreground"> + 개인정보 처리방침과 이용약관을 버전별로 관리합니다 + </p> + </div> + </div> + + {/* 통계 카드들 */} + <div className="grid gap-4 md:grid-cols-4"> + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">총 버전 수</CardTitle> + <FileText className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.totalVersions}</div> + <p className="text-xs text-muted-foreground"> + 전체 정책 버전 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">개인정보 정책</CardTitle> + <Shield className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.privacyVersions}</div> + <p className="text-xs text-muted-foreground"> + 버전 수 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">이용약관</CardTitle> + <FileText className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.termsVersions}</div> + <p className="text-xs text-muted-foreground"> + 버전 수 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">최근 업데이트</CardTitle> + <Clock className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {data.stats.lastUpdate + ? new Date(data.stats.lastUpdate).toLocaleDateString('ko-KR') + : 'N/A' + } + </div> + <p className="text-xs text-muted-foreground"> + 마지막 정책 변경 + </p> + </CardContent> + </Card> + </div> + + {/* 현재 활성 정책들 */} + <div className="grid gap-6 md:grid-cols-2"> + <CurrentPolicyCard + title="개인정보 처리방침" + icon={<Shield className="h-5 w-5" />} + policy={data.currentPolicies.privacy_policy} + type="privacy_policy" + /> + <CurrentPolicyCard + title="이용약관" + icon={<FileText className="h-5 w-5" />} + policy={data.currentPolicies.terms_of_service} + type="terms_of_service" + /> + </div> + + <Separator /> + + {/* 클라이언트 컴포넌트로 편집 기능 제공 */} + <Suspense fallback={<PolicyManagementSkeleton />}> + <PolicyManagementClient initialData={data} /> + </Suspense> + </div> + ) +} + +// 현재 정책 카드 컴포넌트 +function CurrentPolicyCard({ title, icon, policy, type }) { + if (!policy) { + return ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + {icon} + {title} + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-8 text-muted-foreground"> + <p>아직 등록된 정책이 없습니다</p> + <p className="text-sm mt-2">새 버전을 생성해주세요</p> + </div> + </CardContent> + </Card> + ) + } + + return ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + {icon} + {title} + <Badge variant="secondary">v{policy.version}</Badge> + </CardTitle> + <CardDescription> + 현재 활성 정책 • 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-3"> + {/* 정책 내용 미리보기 */} + <div className="bg-muted/50 p-3 rounded-md text-sm max-h-32 overflow-hidden"> + <div className="line-clamp-4"> + {policy.content?.replace(/#{1,6}\s+/g, '').replace(/\*\*(.*?)\*\*/g, '$1').substring(0, 200)}... + </div> + </div> + + {/* 메타 정보 */} + <div className="flex items-center justify-between text-xs text-muted-foreground"> + <div className="flex items-center gap-1"> + <Calendar className="h-3 w-3" /> + 생성: {new Date(policy.createdAt).toLocaleDateString('ko-KR')} + </div> + <div className="flex items-center gap-1"> + <User className="h-3 w-3" /> + 관리자 + </div> + </div> + </div> + </CardContent> + </Card> + ) +} + +// 로딩 스켈레톤 +function PolicyManagementSkeleton() { + return ( + <div className="space-y-4"> + <div className="h-8 bg-muted animate-pulse rounded" /> + <div className="grid gap-4 md:grid-cols-2"> + <div className="h-32 bg-muted animate-pulse rounded" /> + <div className="h-32 bg-muted animate-pulse rounded" /> + </div> + <div className="h-96 bg-muted animate-pulse rounded" /> + </div> + ) +}
\ No newline at end of file |
